<template>
  <div class="triangle-chart">
    <div class="chart-main"></div>
    <div class="tool-tip" v-show="visible" ref="tooltip">
      <div class="topic">
        <span v-text="tooltip.topic"></span>
        <span v-if="tooltip.name !== ''" v-text="' -- ' + tooltip.name"></span>
      </div>
      <div
        class="item-info"
        v-for="(item, index) in tooltip.items"
        :key="index"
      >
        <span v-text="item.label + ' : '"></span>
        <span v-text="item.value + '%'"></span>
      </div>
    </div>
  </div>
</template>
<script lang="ts">
import { Vue, Component, Ref } from "vue-property-decorator";
import * as d3 from "d3";
import Icon from "./Icon";
import { scaleLinear, ScaleLinear } from "d3";

type D3Selection = d3.Selection<any, any, any, any>;

@Component({
  name: "TriangleChart",
  components: {}
})
export default class TriangleChart extends Vue {
  /**
   * svg宽度
   */
  private w!: number;
  /**
   * svg 高度
   */
  private h!: number;
  /**
   * 三角形边长
   */
  private side!: number;
  /**
   * x轴偏移量
   */
  private offsetX!: number;
  /**
   * y轴偏移量
   */
  private offsetY!: number;

  /**
   * 图形边距
   */
  private padding!: number;

  /**
   * 图形参数配置
   */
  private config!: TriangleChartConfig;

  /**
   * 三角形的高
   */
  private vh!: number;

  private svg!: D3Selection;

  private zoom!: D3Selection;

  private defs!: D3Selection;

  private xScale!: ScaleLinear<any, any, any>;

  private visible: boolean = false;

  private tooltip: any = {
    topic: "",
    name: "",
    items: []
  };

  @Ref("tooltip")
  private $ToolTip!: HTMLDivElement;

  setOptions(config: TriangleChartConfig) {
    this.config = config;

    this.padding = this.config.padding;

    const rect = this.$el.getBoundingClientRect();
    this.w = rect.width;
    this.h = rect.height;

    if (this.w <= this.h) {
      this.side = this.w - 2 * this.padding;
      this.offsetX = this.padding;
      this.offsetY = (this.h - (this.side * Math.sqrt(3)) / 2) / 2;
    } else {
      this.side = ((this.h - 2 * this.padding) * 2) / Math.sqrt(3);
      this.offsetX = (this.w - this.side) / 2;
      this.offsetY = this.padding;
    }

    this.vh = this.side * Math.sin(Math.PI / 3);

    this.initSvg();
    this.initStyle();
    this.initAxises();
    this.addExtraLines();
    this.addExtraPoints();
    this.addExtraLabels();
    this.initSplitLines();
    this.addPointRefers();
    this.addArea();
    this.addExtraLabels();
    this.addLegends();
    this.bindEvents();
  }

  initSvg() {
    const $main = this.$el.querySelector(".chart-main") as HTMLDivElement;
    $main.innerHTML = "";

    this.svg = d3
      .select($main)
      .append("svg")
      .attr("width", this.w)
      .attr("height", this.h);

    this.defs = this.svg.append("defs");
    this.zoom = this.svg.append("g").classed("zoom", true);
  }

  private initStyle() {
    const style = this.defs.append("style");
    style.text(
      [
        `.x-axis-group-ticks .tick text{
          fill:${this.config.axies?.x?.color || "#000"}
        }`,
        `.x-axis-group-ticks .tick line{
          stroke:${this.config.axies?.x?.color || "#000"}
        }`,
        `.x-axis-group-ticks path{
          stroke:${this.config.axies?.x?.color || "#000"}
        }`,
        `.y-axis-group-ticks .tick text{
          fill:${this.config.axies?.y?.color || "#000"}
        }`,
        `.y-axis-group-ticks .tick line{
          stroke:${this.config.axies?.y?.color || "#000"}
        }`,
        `.y-axis-group-ticks path{
          stroke:${this.config.axies?.y?.color || "#000"}
        }`,
        `.z-axis-group-ticks .tick text{
          fill:${this.config.axies?.z?.color || "#000"}
        }`,
        `.z-axis-group-ticks .tick line{
          stroke:${this.config.axies?.z?.color || "#000"}
        }`,
        `.z-axis-group-ticks path{
          stroke:${this.config.axies?.z?.color || "#000"}
        }`,
        `.point-item{
          cursor:pointer;
        }`,
        `.point-item:hover{
          opacity:0.6;
        }`
      ].join("")
    );
  }

  private initAxises() {
    this.xScale = scaleLinear()
      .range([this.offsetX, this.offsetX + this.side])
      .domain([0, 100]);

    const dataSet = [
      {
        range: [this.offsetX, this.offsetX + this.side],
        domain: [0, 100],
        rotate: 0,
        origin: [0, 0],
        label: this.config.axies?.x?.title || "X轴",
        id: "x-axis",
        axis: d3.axisBottom,
        dir: 1,
        config: this.config.axies?.x
      },
      {
        range: [this.offsetX + this.side, this.offsetX],
        domain: [0, 100],
        rotate: 60,
        origin: [this.offsetX + this.side, this.h - this.offsetY],
        label: this.config.axies?.y?.title || "Y轴",
        id: "y-axis",
        axis: d3.axisTop,
        dir: -1,
        config: this.config.axies?.y
      },
      {
        range: [this.offsetX + this.side, this.offsetX],
        domain: [0, 100],
        rotate: 300,
        origin: [this.offsetX, this.h - this.offsetY],
        label: this.config.axies?.z?.title || "Z轴",
        id: "z-axis",
        axis: d3.axisTop,
        dir: -1,
        config: this.config.axies?.z
      }
    ];

    this.zoom
      .append("g")
      .classed("axises", true)
      .selectAll("g")
      .data(dataSet)
      .enter()
      .append("g")
      .attr("id", (d) => d.id)
      .classed("axis-item", true);

    for (let i = 0; i < dataSet.length; i++) {
      const data = dataSet[i];
      const target = this.zoom.select(`#${data.id}`) as D3Selection;
      const inner = target
        .append("g")
        .attr("transform", `translate(0, ${this.h - this.offsetY})`);

      const axisGroup = inner
        .append("g")
        .classed("axis-group-item", true)
        .classed(data.id + "-group-ticks", true);
      const scale = d3.scaleLinear().range(data.range).domain(data.domain);

      const axis = data
        .axis(scale)
        .ticks(4)
        .tickSize(8)
        .tickPadding(7)
        .tickFormat((d) => d + "%");
      axisGroup.call(axis);

      const dash = inner.append("g");
      if (this.config.showGrid !== false) {
        this.appendDashLines(dash, data);
      }

      this.appendAxisLabel(inner);
      target.attr(
        "transform",
        `rotate(${data.rotate}, ${data.origin[0]} ${data.origin[1]})`
      );
    }
  }

  private addExtraLines() {
    if (!this.config.extraLines) return;
    this.zoom
      .append("g")
      .classed("extra-lines", true)
      .selectAll("path")
      .data(this.config.extraLines)
      .enter()
      .append("path")
      .classed("extra-line-item", true)
      .attr("d", (d) => {
        const path = d3.path();
        d.points.forEach((p, i) => {
          const { pt } = this.getFullPosition(p.x, p.y);
          if (i === 0) {
            path.moveTo(pt.x, pt.y);
          } else {
            path.lineTo(pt.x, pt.y);
          }
        });
        return path.toString();
      })
      .style("fill", "none")
      .style("stroke", (d) => d.lineStyle?.color || "#000")
      .style("stroke-width", (d) => d.lineStyle?.width || 1)
      .style("stroke-dasharray", (d) =>
        d.lineStyle?.type === "dash" ? "6" : ""
      );
  }
  private addExtraPoints() {
    if (!this.config.extraPoints) return;
    this.zoom.append("g").classed("extra-points", true);
  }
  private addExtraLabels() {
    if (!this.config.extraLabels) return;
    const g = this.zoom.append("g").classed("extra-labels", true);
    const exts = this.config.extraLabels;
    g.selectAll("text")
      .data(exts)
      .enter()
      .append("text")
      .text((d) => d.label)
      .attr("x", (d) => {
        const { x } = this.getRealPosition(d.point.x, d.point.y);
        return x;
      })
      .attr("y", (d) => {
        const { y } = this.getRealPosition(d.point.x, d.point.y);
        return y;
      })
      .style("text-anchor", "middle")
      .style("dominant-baseline", "middle")
      .style("font-size", (d) => d.fontSize || 12)
      .style("font-weight", (d) => d.fontWeight || "normal")
      .style("fill", (d) => d.color || "#000");
  }

  private appendDashLines($g: D3Selection, data: any) {
    const dataSet = [4, 3, 2, 1];
    const h = (Math.sqrt(3) / 2) * this.side;
    const block = this.side / 10;
    const config = data.config as TriangleChartAxisConfig;
    $g.selectAll("path")
      .data(dataSet)
      .enter()
      .append("path")
      .attr("d", (d: number, i) => {
        const path = d3.path();
        const y = (0 - (1 - d / 5)) * h * data.dir;
        path.moveTo(this.offsetX + (i + 1) * block, y);
        path.lineTo(this.w - this.offsetX - (i + 1) * block, y);
        return path.toString();
      })
      .classed(data.id + "-dashline", true)
      .style("stroke-width", 1)
      .style("stroke", config.color || "#000")
      .style("stroke-dasharray", "6");
  }

  private appendAxisLabel($g: D3Selection) {
    const data = $g.datum() as any;
    const _this = this;
    const config = data.config as TriangleChartAxisConfig;

    $g.style("font-size", (this.config.axies?.label?.fontSize || 12) + "px")
      .style("font-weight", this.config.axies?.label?.bold ? "bold" : "")
      .append("text")
      .text(data.label)
      .attr("x", function (this: SVGTextElement) {
        return _this.offsetX + _this.side / 2;
      })
      .attr("y", data.dir * 30)
      .attr("dy", data.dir + "em")
      .classed(data.id + "-label", true)
      .classed("axis-label", true)
      .style("fill", config.color || "#000");
  }

  private addPointRefers() {
    const $pointBox = this.zoom.append("g").classed("point-box", true);
    this.config.series.forEach((d, i) => {
      if (!d.label) {
        d.label = "分组" + i;
      }
      d.id = Icon.createIconMapping(d.icon || {}, this.defs);
      d.points.forEach((p) => {
        p.seryName = d.label + "";
        p.seryId = d.id;
      });
      const $g = $pointBox.append("g").classed(d.id, true);
      this.addPoints(d, d.id, $g);
    });
  }

  private addPoints(
    config: TriangleChartSeryConfig,
    symbolId: string,
    $g: D3Selection
  ) {
    $g.attr("group", symbolId)
      .attr("id", "points_" + symbolId)
      .attr(
        "transform",
        `translate(${this.offsetX}, ${this.h - this.offsetY})`
      );

    const size = config.icon?.size || Icon.defaultIconSize;

    const rate = (this.h - 2 * this.offsetY) / ((100 * Math.sqrt(3)) / 2);

    config.points.forEach((d) => {
      if (this.isPointUsable(d)) {
        const $point = $g.append("use");

        const y = (-1 * rate * d.y * Math.sqrt(3)) / 2;
        const x = (-1 * y) / Math.sqrt(3) + d.x * rate;

        $point
          .attr("xlink:href", "#" + symbolId)
          .attr("width", size)
          .attr("height", size)
          .attr("x", x)
          .attr("y", y)
          .attr(
            "transform",
            `translate(${(-1 * size) / 2}, ${(-1 * size) / 2})`
          )
          .classed("point-item", true);

        $point.datum({
          point: d,
          origin: config
        });
      }
    });
  }

  private addLegends() {
    const $g = this.svg
      .append("g")
      .classed("legends", true)
      .attr("transform", `translate(${this.w - 40}, 25)`);
    const $legend = $g
      .selectAll("g")
      .data(this.config.series)
      .enter()
      .append("g")
      .classed("legend-item", true);

    $legend
      .append("text")
      .text((d) => {
        return d.label + "";
      })
      .attr("x", "-5")
      .attr("y", 0)
      .style("font-size", "12px")
      .style("text-anchor", "end")
      .style("dominant-baseline", "middle");

    $legend
      .attr("transform", (d, i) => {
        return `translate(0, ${i * 25})`;
      })
      .append("use")
      .attr("width", 15)
      .attr("height", 15)
      .attr("x", 0)
      .attr("y", -7.5)
      .attr("xlink:href", (d) => "#" + d.id);
  }

  /**
   * 根据三角图点为数据获取真实的svg坐标系坐标
   */
  private getRealPosition(px: number, py: number) {
    const ylen = ((py / 100) * this.side * Math.sqrt(3)) / 2;
    const y1 = this.h - this.offsetY - ylen;
    const x0 = this.xScale(px);
    const y0 = this.h - this.offsetY;
    const x1: number = (y0 - y1) / Math.sqrt(3) + x0;
    return {
      x: x1,
      y: y1
    };
  }

  /**
   * 根据三角图点为数据获取真实的svg坐标系坐标以及x、y、z轴上的投影坐标
   */
  private getFullPosition(x: number, y: number) {
    const pt = this.getRealPosition(x, y);
    // 目标点投影在x轴上的坐标
    const px = this.getRealPosition(x, 0);
    // 目标点投影在y轴上的坐标
    const py = this.getRealPosition(100 - y, y);
    // 目标点投影在z轴上的坐标
    const pz = this.getRealPosition(0, x + y);
    // x轴原点坐标
    const ox = this.getRealPosition(0, 0);
    // y轴原点坐标
    const oy = this.getRealPosition(100, 0);
    // z轴原点坐标
    const oz = this.getRealPosition(0, 100);

    return { pt, px, py, pz, ox, oy, oz };
  }

  /**
   * 添加关注点
   */
  private addArea() {
    if (!this.config.area) return;

    const area = this.config.area;
    if (!this.isPointUsable({ x: area.x, y: area.y, z: area.z })) return;
    const $g = this.zoom.append("g").classed("areas", true);

    // // 目标点坐标
    // const pc = this.getRealPosition(area.x, area.y);
    // // 目标点投影在x轴上的坐标
    // const px = this.getRealPosition(area.x, 0);
    // // 目标点投影在y轴上的坐标
    // const py = this.getRealPosition(100 - area.y, area.y);
    // // 目标点投影在z轴上的坐标
    // const pz = this.getRealPosition(0, 100 - area.z);
    // // x轴原点坐标
    // const ox = this.getRealPosition(0, 0);
    // // y轴原点坐标
    // const oy = this.getRealPosition(100, 0);
    // // z轴原点坐标
    // const oz = this.getRealPosition(0, 100);
    const {
      pt: pc,
      px,
      py,
      pz,
      ox,
      oy,
      oz
    } = this.getFullPosition(area.x, area.y);

    const points = [px, py, pz];
    // 绘制边界线县
    $g.selectAll("line")
      .data(points)
      .enter()
      .append("line")
      .classed("area-range", true)
      .attr("x1", pc.x)
      .attr("y1", pc.y)
      .attr("x2", (d) => d.x)
      .attr("y2", (d) => d.y)
      .style("stroke", area.lineStyle?.color || "#000")
      .style("stroke-width", area.lineStyle?.width || 1)
      .style("stroke-dasharray", area.lineStyle?.type === "dash" ? "6" : "");

    if (!area.label) return;

    const txtArr = [];

    if (area.label?.xyLabel) {
      txtArr.push({
        text: area.label.xyLabel,
        // points: { p1: pc, p2: px, p3: oy, p4: py }
        points: [pc, px, oy, py]
      });
    }

    if (area.label?.yzLabel) {
      txtArr.push({
        text: area.label.yzLabel,
        // points: { p1: pc, p2: py, p3: oz, p4: pz }
        points: [pc, py, oz, pz]
      });
    }

    if (area.label?.zxLabel) {
      txtArr.push({
        text: area.label.zxLabel,
        // points: { p1: pc, p2: pz, p3: ox, p4: px }
        points: [pc, pz, ox, px]
      });
    }

    if (area.label?.yzLabel) {
      txtArr.push({
        text: area.label.yzLabel,
        points: [pc, py, oz, pz]
      });
    }

    $g.append("g")
      .classed("txts", true)
      .selectAll("text")
      .data(txtArr)
      .enter()
      .append("text")
      .text((d) => d.text)
      .attr("x", (d) => d3.mean(d.points, (item) => item.x) as number)
      .attr("y", (d) => d3.mean(d.points, (item) => item.y) as number)
      .style("font-size", area.label.textStyle?.fontSize || 12)
      .style("font-weight", area.label.textStyle?.fontWeight || "normal")
      .style("fill", area.label.textStyle?.color || "#000")
      .style("stroke", "none")
      .style("text-anchor", "middle")
      .style("dominant-baseline", "middle");
  }

  /**
   * 检测三角图点位是否可用 {x,y,z} >= 0 && {x,y,z} <= 100 && x+y+z === 100
   * @param point
   */
  private isPointUsable(point: TriangleChartPoint): boolean {
    const xUsable = point.x >= 0 && point.x <= 100;
    const yUsable = point.y >= 0 && point.y <= 100;
    const zUsable = point.z >= 0 && point.z <= 100;
    const sumAble = point.x + point.y + point.z === 100;

    return xUsable && yUsable && zUsable && sumAble;
  }

  private initSplitLines() {
    if (!this.config.splitLines) return;

    const $g = this.zoom
      .append("g")
      .classed("split-lines", true)
      .selectAll("g")
      .data(this.config.splitLines)
      .enter()
      .append("g")
      .classed("split-line", true)
      .attr("transform", (d) => {
        if (d.axis === "x") return "";
        const deg = d.axis === "y" ? 240 : 120;
        const r = this.side / 2 / Math.sin(Math.PI / 3);
        return `rotate(${deg}, ${this.w / 2} ${this.offsetY + r})`;
      });

    $g.append("path")
      .style("stroke", (d) => d.color || "#000")
      .style("stroke-width", (d) => d.lineStyle?.width || 1)
      .style("stroke-dasharray", function (d) {
        if (d.lineStyle?.type !== "solid") {
          return "6";
        }
        return "";
      })
      .attr("d", (d) => {
        const len = this.side * d.rate;
        const offset = (this.side - len) / 2;
        const x1 = this.offsetX + offset;
        const x2 = this.w - this.offsetX - offset;
        const y = this.h - this.offsetY - (1 - d.rate) * this.vh;
        const path = d3.path();
        path.moveTo(x1, y);
        path.lineTo(x2, y);
        return path.toString();
      });

    $g.append("text")
      .classed("up-label", true)
      .text((d) => d.upLabel || "")
      .attr("x", this.w / 2)
      .attr("y", (d) => {
        const y = this.h - this.offsetY - (1 - d.rate) * this.vh;
        return y - (d.upLabelFromLine || 0);
      })
      .style("font-weight", (d) => d.textStyle?.fontWeight || "normal")
      .style("fill", (d) => d.color || "#000")
      .style("font-size", (d) => (d.textStyle?.fontSize || 12) + "px")
      .style("text-anchor", "middle")
      .style("dominant-baseline", "middle")
      .attr("transform", (d) => {
        if (d.axis === "x") return "";
        const deg = d.axis === "y" ? -240 : -120;
        const x = this.w / 2;
        const y = this.h - this.offsetY - (1 - d.rate) * this.vh;
        return `rotate(${deg}, ${x} ${y - (d.upLabelFromLine || 0)})`;
      });

    $g.append("text")
      .classed("bottom-label", true)
      .text((d) => d.bottomLabel || "")
      .attr("x", this.w / 2)
      .attr("y", (d) => {
        const y = this.h - this.offsetY - (1 - d.rate) * this.vh;
        return y + (d.upLabelFromLine || 0);
      })
      .style("font-weight", (d) => d.textStyle?.fontWeight || "normal")
      .style("fill", (d) => d.color || "#000")
      .style("font-size", (d) => (d.textStyle?.fontSize || 12) + "px")
      .style("text-anchor", "middle")
      .style("dominant-baseline", "middle")
      .attr("transform", (d) => {
        if (d.axis === "x") return "";
        const deg = d.axis === "y" ? -240 : -120;
        const x = this.w / 2;
        const y = this.h - this.offsetY - (1 - d.rate) * this.vh;
        return `rotate(${deg}, ${x} ${y + (d.upLabelFromLine || 0)})`;
      });
  }

  bindEvents() {
    const _this = this;
    this.zoom
      .selectAll(".point-item")
      .on("mouseover", function (e: MouseEvent, data: any) {
        _this.tooltip.topic = data.origin.label;
        _this.tooltip.name = data.point.name || "";
        const items = [];
        items.push({
          label: _this.config.axies?.x?.title,
          value: data.point.x
        });
        items.push({
          label: _this.config.axies?.y?.title,
          value: data.point.y
        });
        items.push({
          label: _this.config.axies?.z?.title,
          value: data.point.z
        });

        _this.tooltip.items = items;

        _this.$ToolTip.style.left = e.clientX + 15 + "px";
        _this.$ToolTip.style.top = e.clientY + 15 + "px";
        _this.visible = true;
      })
      .on("mouseout", function () {
        _this.visible = false;
      });
  }
}
</script>
<style lang="scss" scoped>
.triangle-chart {
  width: 100%;
  height: 100%;
  position: relative;
  .chart-main {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
  }
  .tool-tip {
    position: fixed;
    z-index: 9999;
    left: 0;
    min-width: 150px;
    top: 0;
    background-color: rgba(0, 0, 0, 0.8);
    color: #fff;
    text-align: left;
    padding: 10px;
    border-radius: 6px;
    transition: all ease 0.3s;
    .topic {
      font-size: 14px;
      font-weight: bold;
      margin-bottom: 10px;
    }
    .item-info {
      font-size: 12px;
      margin-top: 3px;
    }
  }
}
</style>
